package org.xlp.db.xml;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.xlp.db.xml.ele.ComEle;
import org.xlp.db.xml.ele.ParamConstants;
import org.xlp.db.xml.ele.ParamEle;
import org.xlp.db.xml.ele.SQLEle;
import org.xlp.utils.XLPCharsetUtil;
import org.xlp.utils.XLPStringUtil;
import org.xlp.utils.io.XLPIOUtil;
import org.xml.sax.InputSource;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

/**
 * sql配置文件处理接口抽象实现类
 */
public abstract class AbstractSQLXmlFileHandler implements ISQLXmlFileHandler {
    /**
     * 当前解析的sql配置文件信息
     */
    private String fileDescription = "";

    /**
     * 解析sql配置文件所使用的字符编码名称，默认使用UTF-8
     */
    private String charsetName = XLPCharsetUtil.UTF8;

    /**
     * 标记解析Xml文件时是否对约束文件进行验证，false：不校验，true：校验，默认不校验
     */
    private boolean validate = false;

    /**
     * 获取解析Xml文件时是否对约束文件进行验证，false：不校验，true：校验，默认不校验
     * @return
     */
    public boolean isValidate() {
        return validate;
    }

    /**
     * 设置解析Xml文件时是否对约束文件进行验证，false：不校验，true：校验，默认不校验
     * @param validate
     */
    public void setValidate(boolean validate) {
        this.validate = validate;
    }

    /**
     * 获取字符编码
     * @return
     */
    public String getCharsetName() {
        return charsetName;
    }

    /**
     * 设置字符编码
     * @param charsetName
     */
    public void setCharsetName(String charsetName) {
        if(!XLPStringUtil.isEmpty(charsetName)) {
            this.charsetName = charsetName;
        }
    }

    /**
     * 处理sql配置文件，把配置文件信息处理成SQLConfigurationFileInfo对象
     *
     * @param sqlXmlFilePath sql配置文件
     * @param chain          sql配置文件解析责任链
     * @return
     * @throws SQLXmlFileParseException 当处理失败时，则抛出该异常
     */
    @Override
    public SQLConfigurationFileInfo handle(String sqlXmlFilePath, ISQLXmlFileHandlerChain chain)
            throws SQLXmlFileParseException {
        if(match(sqlXmlFilePath)){
            fileDescription = sqlXmlFilePath;
            return doHandle(sqlXmlFilePath);
        } else {
            return chain.handle(sqlXmlFilePath);
        }
    }

    /**
     * 判断给定的路径是否符合该sql配置文件解析器，进行解析
     * @param sqlXmlFilePath
     * @return 返回 true：该解析器进行解析，false：则不进行解析，交给下一个解析器去解析
     * @see AbstractSQLXmlFileHandler#handle(String, ISQLXmlFileHandlerChain)
     */
    protected boolean match(String sqlXmlFilePath) {
        return !XLPStringUtil.isEmpty(sqlXmlFilePath)
                && sqlXmlFilePath.endsWith(FilePathPrefixConstant.XML_FILE_SUFFIX);
    }

    /**
     * 解析给定的sql配置文件
     * @param sqlXmlFilePath
     * @return
     * @throws SQLXmlFileParseException 当处理失败时，则抛出该异常
     */
    protected SQLConfigurationFileInfo doHandle(String sqlXmlFilePath) throws SQLXmlFileParseException {
        InputStream inputStream = null;
        try {
            inputStream = toInputStream(sqlXmlFilePath);
            //xml文件解析开始
            SAXReader reader = new SAXReader();
            //设置解析字符编码
            reader.setEncoding(this.charsetName);
            //设置读取xml文件时是否对约束文件进行验证
            setResolveEntity(reader);

            //获取xml文件文档对象
            Document document = reader.read(inputStream);

            // 获取根元素
            Element rootElement = document.getRootElement();

            SQLConfigurationFileInfo sqlConfigurationFileInfo = new SQLConfigurationFileInfo();
            //设置当前解析的文件描述信息
            sqlConfigurationFileInfo.setFileDescription(sqlXmlFilePath);

            //处理sqls元素
            sqlConfigurationFileInfo.setSqlsElementInfos(getSqlsElementInfos(rootElement));

            //处理file元素
            sqlConfigurationFileInfo.setSqlConfigurationFilePaths(getFilePaths(rootElement));

            return sqlConfigurationFileInfo;
        } catch (DocumentException e) {
            throw new SQLXmlFileParseException("解析【" + sqlXmlFilePath + "】该配置文件失败：", e);
        } finally {
            XLPIOUtil.closeInputStream(inputStream);
        }
    }

    private void setResolveEntity(SAXReader reader) {
        if (!validate && reader != null){
            reader.setEntityResolver((publicId, systemId) -> new InputSource(new ByteArrayInputStream("".getBytes())));
        }
    }

    /**
     * 处理file元素内容
     * @param rootElement
     */
    protected List<String> getFilePaths(Element rootElement) {
        // 获取特定名称的子元素集合（file）
        List<Element> fileList = rootElement.elements("file");
        List<String> filePaths = new ArrayList<>(fileList.size());
        for (Element element : fileList) {
            String refFile = element.attributeValue("ref-file");
            if (!XLPStringUtil.isEmpty(refFile)){
                filePaths.add(refFile);
            }
        }
        return filePaths;
    }

    /**
     * 处理sqls元素内容
     * @param rootElement
     */
    protected List<SQLsElementInfo> getSqlsElementInfos(Element rootElement) {
        // 获取特定名称的子元素集合（sqls）
        List<Element> sqlsElements = rootElement.elements("sqls");
        List<SQLsElementInfo> infos = new ArrayList<>(sqlsElements.size());

        SQLsElementInfo info;
        List<ComEle> comEles;
        List<SQLEle> sqlEles;
        for (Element element : sqlsElements) {
            info = new SQLsElementInfo();
            info.setId(XLPStringUtil.emptyTrim(element.attributeValue("id")));
            info.setMark(element.attributeValue("mark"));
            comEles = getComEles(element);
            sqlEles = getSqlEles(element, comEles);
            info.setComEles(comEles);
            info.setSqlEles(sqlEles);
            setParentId(info);
            infos.add(info);
        }
        return infos;
    }

    /**
     * 给SQLsElementInfo对象中字段设置parentID
     * @param info
     */
    private void setParentId(SQLsElementInfo info) {
        info.getComEles().forEach(item -> item.setParentId(info.getId()));
        info.getSqlEles().forEach(item -> item.setParentId(info.getId()));
    }

    /**
     * 解析com元素
     * @param element
     * @return
     */
    private List<ComEle> getComEles(Element element) {
        List<Element> elements = element.elements("com");
        List<ComEle> comEles = new ArrayList<>(elements.size());
        ComEle comEle;
        for (Element ele : elements) {
            comEle = new ComEle();
            comEle.setId(XLPStringUtil.emptyTrim(ele.attributeValue("id")));
            comEle.setContent(ele.getTextTrim());
            comEles.add(comEle);
        }
        return comEles;
    }

    private List<SQLEle> getSqlEles(Element element, List<ComEle> comEles){
        List<Element> elements = element.elements("sql");
        List<SQLEle> sqlEles = new ArrayList<>(elements.size());
        for (Element ele : elements) {
            sqlEles.add(getSqlEle(ele, comEles));
        }
        return sqlEles;
    }

    /**
     * 解析sql-ele元素
     * @param sqlElement
     * @return
     */
    private SQLEle getSqlEle(Element sqlElement, List<ComEle> comEles) {
        SQLEle sqlEle = new SQLEle();
        sqlEle.setFileDescription(fileDescription);
        sqlEle.setId(XLPStringUtil.emptyTrim(sqlElement.attributeValue("id")));
        sqlEle.setComment(sqlElement.attributeValue("comment"));
        //设置orderby 内容
        Element e1 = sqlElement.element("order-by");
        sqlEle.setOrderBy(e1 == null ? "" : e1.getTextTrim());
        //设置group by 内容
        e1 = sqlElement.element("group-by");
        sqlEle.setGroupBy(e1 == null ? "" : e1.getTextTrim());

        e1 = sqlElement.element("sql-ele");
        String sqlContent = e1 == null ? "" : e1.getTextTrim();
        sqlContent = replaceContent(sqlContent, comEles);
        sqlEle.setSqlContent(sqlContent);

        //设置参数信息
        sqlEle.setParamEles(getParmEles(sqlElement));

        return sqlEle;
    }

    /**
     * 解析sql元素里的param元素类容
     * @param sqlElement
     * @return
     */
    private List<ParamEle> getParmEles(Element sqlElement) {
        List<Element> elements = sqlElement.elements("param");
        List<ParamEle> paramEles = new ArrayList<>(elements.size());
        ParamEle paramEle;
        for (Element element : elements) {
            paramEle = getParamEle(element);
            paramEles.add(paramEle);
        }
        return paramEles;
    }

    /**
     * 解析param元素
     * @param element
     * @return
     */
    private ParamEle getParamEle(Element element) {
        ParamEle paramEle = new ParamEle();
        String flag = element.attributeValue("flag");
        paramEle.setFlag(toFlag(flag));

        String joint = XLPStringUtil.emptyTrim(element.attributeValue("joint"));
        paramEle.setJoint("true".equals(joint));
        String trim = XLPStringUtil.emptyTrim(element.attributeValue("trim"));
        paramEle.setTrim("true".equals(trim));

        paramEle.setBracket(toBracket(element.attributeValue("bracket")));

        paramEle.setCol(XLPStringUtil.emptyTrim(element.attributeValue("col")));

        String type = element.attributeValue("type");
        paramEle.setType(toType(type));

        String op = element.attributeValue("op");
        paramEle.setOp(toOp(op));

        paramEle.setFn(XLPStringUtil.emptyTrim(element.attributeValue("fn")));
        paramEle.setDefaultV(element.attributeValue("f_v"));

        String connector = element.attributeValue("connector");
        paramEle.setConnector(toConnector(connector));
        return paramEle;
    }

    /**
     * 解析成Connector枚举
     * @param connector
     * @return
     */
    private ParamConstants.Connector toConnector(String connector) {
        if (connector == null) return ParamConstants.Connector.blank;
        connector = XLPStringUtil.emptyTrim(connector);
        try {
            return ParamConstants.Connector.valueOf(connector);
        } catch (IllegalArgumentException e) {
            throw new SQLXmlFileParseException("[" + fileDescription + "]该SQL配置文件中param元素connector值["
                    + connector + "]不合法，该值必须是（and/or/blank）");
        }
    }

    /**
     * 解析成Op枚举
     * @param op
     * @return
     */
    private ParamConstants.Op toOp(String op) {
        op = XLPStringUtil.emptyTrim(op);
        if (op.length() == 0) return null;
        try {
            return ParamConstants.Op.valueOf(op);
        } catch (IllegalArgumentException e) {
            throw new SQLXmlFileParseException("[" + fileDescription + "]该SQL配置文件中param元素op值["
                    + op + "]不合法，该值必须是（like/not_like/eq/not_eq/lt/gt/le/ge/in/not_in）");
        }
    }

    /**
     * 解析成Type枚举
     * @param type
     * @return
     */
    private ParamConstants.Type toType(String type) {
        if (type == null) return ParamConstants.Type.STRING;
        type = XLPStringUtil.emptyTrim(type);
        try {
            return ParamConstants.Type.valueOf(type);
        } catch (IllegalArgumentException e) {
            throw new SQLXmlFileParseException("[" + fileDescription + "]该SQL配置文件中param元素type值["
                    + type + "]不合法，该值必须是（INT|LONG|BIGINT|BIGDECIMAL|DOUBLE|STRING）");
        }
    }

    /**
     * 解析成Bracket枚举
     * @param bracket
     * @return
     */
    private ParamConstants.Bracket toBracket(String bracket) {
        if (bracket == null) return ParamConstants.Bracket.blank;
        bracket = XLPStringUtil.emptyTrim(bracket);
        try {
            return ParamConstants.Bracket.valueOf(bracket);
        } catch (IllegalArgumentException e) {
            throw new SQLXmlFileParseException("[" + fileDescription + "]该SQL配置文件中param元素bracket值["
                    + bracket + "]不合法，该值必须是（lb/rb/blank）");
        }
    }

    /**
     * 解析成flag枚举类型
     * @param flag
     * @return
     */
    private ParamConstants.Flag toFlag(String flag) {
        flag = XLPStringUtil.emptyTrim(flag);
        try {
            return ParamConstants.Flag.valueOf(flag);
        } catch (IllegalArgumentException e) {
            throw new SQLXmlFileParseException("[" + fileDescription + "]该SQL配置文件中param元素flag值["
                    + flag + "]不合法，该值必须是（obj/map）");
        }
    }

    /**
     * 替换成公共内容
     * @param sqlContent
     * @param comEles
     * @return
     */
    private String replaceContent(String sqlContent, List<ComEle> comEles) {
        if (!XLPStringUtil.isEmpty(sqlContent)) {
            for (ComEle comEle : comEles) {
                //替换成指定ID对应的com元素内容
                // select * from a where {#com元素ID} -> select * from a where [com元素内容]
                sqlContent = sqlContent.replace("{#" + comEle.getId() + "}",
                        comEle.getContent());
            }
        }
        return sqlContent;
    }

    /**
     * 把指定的文件描述路径转换成文件输入流
     * @param sqlXmlFilePath
     * @return
     */
    protected abstract InputStream toInputStream(String sqlXmlFilePath);
}
